﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ICSharpCode.SharpZipLib.Zip;
using System.IO;
using SharpShare.Storage.Security;
using SharpShare.Storage.Searching;
using SharpShare.Utilities;

namespace SharpShare.Storage.Zip {
    public class ZipArchiveStorageItem : IStorageContainer, IStorageFile, IStorageProvider, IEquatable<ZipArchiveStorageItem> {
        public const string PathSeparator = "/";

        private static readonly Guid MetadataExtraDataPrefix = new Guid("b2a393ea-aa9c-408d-b8c1-dde899b48012");

        private ZipFile _zipFile;
        private bool _hasFixedDirectories;

        private ZipArchiveStorageItem(ZipFile zipFile, RelinquishableStream stream, string path, IStorageFile storageFile, bool readOnly) {
            if (string.IsNullOrEmpty(path)) {
                path = PathSeparator;
            }

            this.ZipFile = zipFile;
            this.Path = path;
            this.StorageFile = storageFile;
            this.ZipStream = stream;
            this.IsReadOnly = readOnly;
        }

        public ZipArchiveStorageItem(IStorageFile source, bool readOnly = false) {
            if (source == null) {
                throw new ArgumentNullException("source");
            }

            this.Path = PathSeparator;
            this.IsReadOnly = readOnly;

            this.ZipStream = new RelinquishableStream(() => {
                IStorageDataProvider dataProvider = source.Open(StorageFileAccess.Read, StorageFileAccess.Read);
                return new StorageDataStream(dataProvider);
            });

            ZipFile zipFile = new ZipFile(this.ZipStream);
            zipFile.NameTransform = new NoNameTransform();

            this.ZipFile = zipFile;
            this.StorageFile = source;
        }

        public string Path { get; private set; }
        public bool IsReadOnly { get; private set; }

        private bool IsRootPath { get { return (this.Path == PathSeparator); } }
        private bool IsDirectoryPath { get { return this.Path.EndsWith(PathSeparator); } }
        private string[] PathComponents { get { return this.Path.Split(new[] { PathSeparator[0] }, StringSplitOptions.RemoveEmptyEntries); } }

        internal RelinquishableStream ZipStream { get; set; }
        internal ZipFile ZipFile {
            get {
                if (_zipFile == null) {
                    return null;
                }

                lock (_zipFile) {
                    if (!_hasFixedDirectories) {
                        _hasFixedDirectories = true;

                        if (!this.IsReadOnly) {
                            // Modify the ZIP file by adding empty directory entries.
                            var missingPaths = this.ZipFile.ListPaths()
                                .Where(p => p.EndsWith(PathSeparator))
                                .Where(p => this.ZipFile.GetEntry(p) == null)
                                .ToList();

                            BeginUpdatingArchive(() => {
                                foreach (string path in missingPaths) {
                                    this.ZipFile.Add(new ZipEntry(path) {
                                        Size = 0,
                                        CompressedSize = 0
                                    });
                                }
                            });
                        }
                    }
                }

                return _zipFile;
            }
            private set { _zipFile = value; }
        }

        internal ZipEntry ZipEntry { get { return this.ZipFile.GetEntry(this.Path); } }
        internal IStorageFile StorageFile { get; private set; }

        #region IStorageItem Members

        public IStorageProvider Provider {
            get {
                if (this.IsRootPath) {
                    return this;
                }

                return Vend(PathSeparator);
            }
        }

        public string Name {
            get {
                if (this.IsRootPath) {
                    string useName = this.StorageFile.Name;
                    if (useName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) {
                        useName = useName.Substring(0, useName.Length - 4);
                    }

                    return useName;
                }

                string[] components = this.PathComponents;
                string name = components[components.Length - 1];
                return name;
            }
        }

        public string Identifier {
            get { return this.Path; }
        }

        public bool Exists {
            get {
                if (this.IsRootPath) {
                    return this.StorageFile.Exists;
                }

                return (this.ZipEntry != null);
            }
        }

        public IStorageContainer Parent {
            get {
                if (this.IsRootPath) {
                    return null;
                }

                string[] components = this.PathComponents;
                string parentPath = (string.Join(PathSeparator, components, 0, components.Length - 1) + PathSeparator);

                return Vend(parentPath);
            }
        }

        public StorageItemAttributes Attributes {
            get {
                return StorageItemAttributes.None;
            }
            set {
                this.DemandExists();
                this.DemandModifiable();
            }
        }

        public StorageAccessList AvailableAccess {
            get {
                if (this.IsReadOnly) {
                    return -StorageAccessList.Modification;
                }

                return StorageAccessList.Empty;
            }
        }

        public DateTime DateCreated {
            get {
                if (this.IsRootPath) {
                    return this.StorageFile.DateCreated;
                }

                this.DemandExists();

                return this.ZipEntry.DateTime;
            }
            set {
                this.DemandExists();
                this.DemandModifiable();
            }
        }

        public DateTime DateModified {
            get {
                if (this.IsRootPath) {
                    return this.StorageFile.DateModified;
                }

                return this.DateCreated;
            }
            set {
                this.DemandExists();
                this.DemandModifiable();
            }
        }

        public IStorageDataProvider OpenMetadata(string name, StorageFileAccess access, StorageFileAccess share, StorageFileMode mode) {
            throw new StorageItemUnauthorizedException();
        }

        bool IStorageItem.RemoveMetadata(string name) {
            return false;
        }

        public IEnumerable<string> ListMetadata() {
            yield break;
        }

        void IStorageItem.Delete() {
            if (this.IsRootPath) {
                this.StorageFile.Delete();
                return;
            }

            this.DemandExists();
            this.DemandModifiable();

            BeginUpdatingArchive(() => {
                this.ZipFile.Delete(this.ZipEntry);
            });
        }

        void IStorageItem.Move(IStorageContainer container, string newNameOrNull) {
            if (this.IsRootPath) {
                this.StorageFile.Move(container, newNameOrNull);
                return;
            }

            this.DemandExists();
            this.DemandModifiable();

            throw new StorageItemInUseException();
        }

        #endregion

        #region IStorageContainer Members

        IStorageLookup IStorageContainer.Contents {
            get {
                this.DemandContainer();

                var childPaths = this.ZipFile.ListChildPaths(this.Path);

                IStorageLookup lookup = childPaths.ToStorageLookup(
                    name => this.Path + name,
                    path => Vend(path));

                return lookup;
            }
        }

        IStorageContainer IStorageContainer.CreateContainer(string name) {
            this.DemandContainer();
            this.DemandModifiable();

            string newContainerPath = string.Join(PathSeparator, this.PathComponents.Concat(new[] { name, string.Empty }));

            BeginUpdatingArchive(() => {
                this.ZipFile.Add(new ZipEntry(newContainerPath) {
                    Size = 0,
                    CompressedSize = 0
                });
            });

            ZipArchiveStorageItem newContainerItem = Vend(newContainerPath);

            return newContainerItem;
        }

        IStorageFile IStorageContainer.CreateFile(string name) {
            this.DemandContainer();
            this.DemandModifiable();

            if (name.StartsWith(".")) {
                throw new StorageItemLockedException();
            }

            string newFilePath = string.Join(PathSeparator, this.PathComponents.Concat(new[] { name }));

            if (this.ZipFile.GetEntry(newFilePath) != null) {
                throw new StorageItemInUseException();
            }

            BeginUpdatingArchive(() => {
                this.ZipFile.Add(new ZipEntry(newFilePath) {
                    Size = 0,
                    CompressedSize = 0
                });
            });

            ZipArchiveStorageItem newFileItem = Vend(newFilePath);

            return newFileItem;
        }

        #endregion

        #region IStorageFile Members

        long IStorageFile.Length {
            get {
                this.DemandFile();

                return this.ZipEntry.Size;
            }
        }

        IStorageDataProvider IStorageFile.Open(StorageFileAccess access, StorageFileAccess share) {
            this.DemandFile();
            this.DemandExists();

            if (this.IsReadOnly && access.HasFlag(StorageFileAccess.Write)) {
                throw new StorageItemLockedException();
            }

            if (!access.HasFlag(StorageFileAccess.Write)) {
                // Requesting read only. Get the read-only, non-seekable input stream from the ZIP file and
                // wrap it around a SeekableStream which makes a non-seekable stream look seekable by persisting
                // read data to a backing stream (which we are using a temporary file that deletes on close).
                Stream inputStream = this.ZipFile.GetInputStream(this.ZipEntry);

                string backingFilename = System.IO.Path.GetTempFileName();
                FileStream backingStream = System.IO.File.Create(backingFilename, 10240, FileOptions.DeleteOnClose);
                SeekableStream seekingStream = new SeekableStream(inputStream, backingStream, this.ZipEntry.Size);

                IStorageDataProvider dataProvider = new StreamStorageDataProvider(seekingStream);

                return dataProvider;
            }

            // Requesting write access.  Need to extract the file to temporary location and
            // expose its data via the ZipEntryStreamStorageDataProvider which updates the archive
            // on close.
            string tempFilename = System.IO.Path.GetTempFileName();
            FileStream tempStream = System.IO.File.Create(tempFilename, 10240, FileOptions.DeleteOnClose);
            using (Stream entryStream = this.ZipFile.GetInputStream(this.ZipEntry)) {
                entryStream.CopyTo(tempStream);
            }

            IStorageDataProvider writeableDataProvider = new ZipEntryStreamStorageDataProvider(tempStream, this);

            return writeableDataProvider;
        }

        #endregion

        #region IStorageProvider Members

        event EventHandler IStorageProvider.ContentsChanged {
            add {

            }
            remove {

            }
        }

        ulong IStorageProvider.AvailableBytes {
            get {
                if (!this.IsRootPath) {
                    throw new StorageItemInvalidTypeException();
                }

                return this.StorageFile.GetStorageProvider().AvailableBytes;
            }
        }

        ulong IStorageProvider.TotalBytes {
            get {
                if (!this.IsRootPath) {
                    throw new StorageItemInvalidTypeException();
                }

                return this.StorageFile.GetStorageProvider().TotalBytes;
            }
        }

        IStorageSearchEngine IStorageProvider.SearchEngine {
            get {
                return null;
            }
        }

        #endregion

        #region ICastable<IStorageContainer> Members

        IStorageContainer ICastable<IStorageContainer>.TryCast() {
            if (this.IsDirectoryPath) {
                return this;
            }

            return null;
        }

        #endregion

        #region ICastable<IStorageFile> Members

        IStorageFile ICastable<IStorageFile>.TryCast() {
            if (!this.IsDirectoryPath) {
                return this;
            }

            return null;
        }

        #endregion

        #region ICastable<IStorageProvider> Members

        IStorageProvider ICastable<IStorageProvider>.TryCast() {
            if (this.IsRootPath) {
                return this;
            }

            return null;
        }

        #endregion

        #region IEquatable<ZipArchiveStorageItem> Members

        public bool Equals(ZipArchiveStorageItem other) {
            if (other == null) {
                return false;
            }

            bool equal = (this.StorageFile.Equals(other.StorageFile) && this.Path == other.Path);

            return equal;
        }

        #endregion

        public override string ToString() {
            return string.Format("[ZIP Archive] {0} (Source: {1})", this.Path, this.StorageFile);
        }

        public override bool Equals(object obj) {
            return Equals(obj as ZipArchiveStorageItem);
        }

        public override int GetHashCode() {
            return (this.StorageFile.GetHashCode() ^ this.Path.GetHashCode());
        }

        internal void BeginUpdatingArchive(Action updater) {
            this.DemandModifiable();

            lock (this.ZipFile) {
                this.ZipFile.Update(updater);
            }
        }

        private void DemandModifiable() {
            if (this.IsReadOnly) {
                throw new StorageItemUnauthorizedException();
            }
        }

        private ZipArchiveStorageItem Vend(string path) {
            return new ZipArchiveStorageItem(this.ZipFile, this.ZipStream, path, this.StorageFile, this.IsReadOnly);
        }

        private static IDictionary<string, string> DeserializeMetadata(byte[] data) {
            Dictionary<string, string> metadata = new Dictionary<string, string>();

            if (data == null || data.Length < 16) {
                return metadata;
            }

            byte[] dataPrefix = new byte[16];
            Array.Copy(data, dataPrefix, 16);

            if (new Guid(dataPrefix) != MetadataExtraDataPrefix) {
                return metadata;
            }

            try {
                using (MemoryStream stream = new MemoryStream(data)) {
                    using (BinaryReader reader = new BinaryReader(stream)) {
                        while (stream.Position < stream.Length) {
                            string key = reader.ReadString();
                            string value = reader.ReadString();
                            metadata[key] = value;
                        }
                    }
                }
            } catch {
                metadata.Clear();
            }

            return metadata;
        }
        private static byte[] SerializeMetadata(IDictionary<string, string> metadata) {
            if (metadata == null) {
                return new byte[0];
            }

            using (MemoryStream stream = new MemoryStream()) {
                stream.Write(MetadataExtraDataPrefix.ToByteArray(), 0, 16);

                using (BinaryWriter writer = new BinaryWriter(stream)) {
                    foreach (var pair in metadata) {
                        writer.Write(pair.Key);
                        writer.Write(pair.Value);
                    }
                }

                return stream.ToArray();
            }
        }

        private sealed class NoNameTransform : ICSharpCode.SharpZipLib.Core.INameTransform {
            #region INameTransform Members

            public string TransformDirectory(string name) {
                return name;
            }

            public string TransformFile(string name) {
                return name;
            }

            #endregion
        }
    }
}
